/*
 * File     : FastCharset.java
 * Created  : 5 May 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.string;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.MemoryAccess;
import com.googlecode.dni.library.Library;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * <p>
 *  Provides a service to find a faster string encoding / decoding
 *  implementation.
 * </p>
 * <p>
 *  We want to avoid the super-generic capabilities of {@link Charset}.
 *  Instead, we can use faster code for the really common charsets (like UTF-8,
 *  UTF-16LE, ISO-8859-1).  Also, we can write to and read from native memory
 *  directly.  This base class provides the slow path.
 * </p>
 *
 * @author Matthew Wilson
 */
public class FastCharset
{

    private static final Map< String, FastCharset > INSTANCES = new HashMap< String, FastCharset >();

    private static final FastCharset DEFAULT;

    static
    {
        initFast( new AsciiCoder.AsciiFastCharset() );
        initFast( new Iso8859_1Coder.Iso8859_1FastCharset() );
        initFast( new Utf16Coder.Utf16FastCharset() );
        initFast( new Utf8Coder.Utf8FastCharset() );
        initFast( new Windows1252Coder.Windows1252FastCharset() );

        DEFAULT = getInstance( DniInternal.PLATFORM.getDefaultCharset() );
        INSTANCES.put( Library.DEFAULT_CHARSET, DEFAULT );
        INSTANCES.put( "", DEFAULT );

        FastCharset legacy = getInstance( DniInternal.PLATFORM.getWideCharset() );
        INSTANCES.put( Library.WIDE_CHARSET, legacy );
    }


    private final String staticClassName;

    private final String name;

    private final Charset charset;

    private final int codeSize;

    private final float maxBytesPerChar;

    /**
     * @param name
     *            the charset name
     * @param staticClassName
     *            the static class name, if any
     * @param charset
     *            the charset
     * @param codeSize
     *            the size of a charset code (1, 2 or 4)
     */
    FastCharset( final String name,
                 final String staticClassName,
                 final Charset charset,
                 final int codeSize )
    {
        this.name = name;
        this.staticClassName = staticClassName;
        this.charset = charset;
        this.codeSize = codeSize;
        this.maxBytesPerChar = charset != null ? charset.newEncoder().maxBytesPerChar() : Float.NaN;
    }

    /**
     * @return the name of the charset
     */
    public final String getName()
    {
        return this.name;
    }

    /**
     * Obtains the name of a static class that has methods to encode quickly.
     * If none is present, <code>null</code> is returned.
     *
     * @return the static class name, or <code>null</code>
     */
    public final String getStaticClassName()
    {
        return this.staticClassName;
    }

    /**
     * Determines the unit size for the coding of strings.
     *
     * @return the code size (1, 2, or 4)
     */
    public final int getCodeSize()
    {
        return this.codeSize;
    }

    /**
     * Obtains an upper bound on the space required for the given string with a
     * null terminator.
     *
     * @param string
     *            the string
     * @return the size in bytes
     */
    public int getByteCount( final String string )
    {
        return (int) ( this.maxBytesPerChar * ( string.length() + 2 ) );
    }

    /**
     * Encodes the given string into the given buffer.
     *
     * @param string
     *            the string
     * @param pointer
     *            the pointer
     *
     * @return the number of bytes used (including terminator)
     */
    public int encode( final String string,
                       final long pointer )
    {
        int count = getByteCount( string );
        ByteBuffer buffer = MemoryAccess.MEMORY_ACCESSOR.getBuffer( pointer, count );

        CharsetEncoder encoder = this.charset.newEncoder();
        encoder.onMalformedInput( CodingErrorAction.REPLACE ).onUnmappableCharacter( CodingErrorAction.REPLACE );
        encoder.encode( CharBuffer.wrap( string ), buffer, true );
        for ( int i = 0; i < getCodeSize(); i++ )
        {
            buffer.put( (byte) 0 );
        }
        return buffer.position();
    }

    /**
     * Encodes the given string into the given buffer.
     *
     * @param string
     *            the string
     * @param pointer
     *            the pointer
     * @param maximumLength
     *            the maximum number of bytes to use
     *
     * @return the number of bytes used (including terminator)
     *
     * @throws IllegalArgumentException
     *              if the buffer was too small
     */
    public int encode( final String string,
                       final long pointer,
                       final int maximumLength )
        throws IllegalArgumentException
    {
        ByteBuffer buffer = MemoryAccess.MEMORY_ACCESSOR.getBuffer( pointer, maximumLength );

        CharsetEncoder encoder = this.charset.newEncoder();
        encoder.onMalformedInput( CodingErrorAction.REPLACE ).onUnmappableCharacter( CodingErrorAction.REPLACE );
        CoderResult result = encoder.encode( CharBuffer.wrap( string ), buffer, true );
        if ( result.isOverflow() || buffer.remaining() < getCodeSize() )
        {
            throw new IllegalArgumentException( "Buffer too small" );
        }

        for ( int i = 0; i < getCodeSize(); i++ )
        {
            buffer.put( (byte) 0 );
        }
        return buffer.position();
    }

    /**
     * Encodes the given string into the given buffer.  The position is then
     * undefined.
     *
     * @param string
     *            the string
     * @param pointer
     *            the pointer
     *
     * @return the number of bytes used (including terminator)
     */
    public int encode( final CharSequence string,
                       final long pointer )
    {
        if ( string instanceof String )
        {
            return encode( (String) string, pointer );
        }
        return encode( string.toString(), pointer );
    }

    /**
     * Reads a null-terminated string.
     *
     * @param pointer
     *            the start of the string
     *
     * @return a string
     */
    public String decode( final long pointer )
    {
        if ( pointer == 0 )
        {
            return null;
        }

        CharsetDecoder decoder = this.charset.newDecoder();
        decoder.onMalformedInput( CodingErrorAction.REPLACE ).onUnmappableCharacter( CodingErrorAction.REPLACE );

        // find the end of the string
        long current = pointer;
        switch ( this.codeSize )
        {
            case 1:
                while ( DniInternal.MEMORY_ACCESSOR.getByte( current ) != 0 )
                {
                    current++;
                }
                break;

            case 2:
                while ( DniInternal.MEMORY_ACCESSOR.getChar( current ) != 0 )
                {
                    current += 2;
                }
                break;

            case 4:
                while ( DniInternal.MEMORY_ACCESSOR.getInt( current ) != 0 )
                {
                    current += 4;
                }
                break;

            default:
                throw new AssertionError( this.codeSize );
        }

        long length = current - pointer;
        if ( length != (int) length )
        {
            throw new IllegalArgumentException( "String too long" );
        }

        if ( length == 0 )
        {
            return "";
        }

        ByteBuffer buffer = DniInternal.MEMORY_ACCESSOR.getBuffer( pointer, (int) length );

        try
        {
            return decoder.decode( buffer ).toString();
        }
        catch ( CharacterCodingException exception )
        {
            throw (InternalError) new InternalError( "Failed to decode string" ).initCause( exception );
        }
    }

    /** {@inheritDoc} */
    @Override
    public String toString()
    {
        return "FastCharset[" + this.name + ";fast=" + ( this.staticClassName != null ) + "]";
    }

    /**
     * Obtains an instance for the given charset name.
     *
     * @param name
     *            the charset name
     *
     * @return an instance
     */
    public static FastCharset getInstance( final String name )
    {
        if ( name == null )
        {
            return DEFAULT;
        }

        synchronized ( INSTANCES )
        {
            String nameLC = name.toLowerCase( Locale.US );
            FastCharset instance = INSTANCES.get( nameLC );
            if ( instance == null )
            {
                Charset charset = Charset.forName( name );
                // almost EVERYTHING (AFAICT) uses a byte-based encoding,
                // except for the UTF types

                int codeSize = 1;
                if ( nameLC.contains( "utf-16" ) )
                {
                    codeSize = 2;
                }
                else if ( nameLC.contains( "utf-32" ) )
                {
                    codeSize = 4;
                }

                if ( nameLC.contains( "utf" ) && nameLC.contains( "bom" ) )
                {
                    // that wouldn't make sense
                    throw new IllegalArgumentException( "Can't use UTF BOM strings" );
                }

                instance = new FastCharset( name, null, charset, codeSize );
            }
            return instance;
        }
    }

    @SuppressFBWarnings( value = "PMB_POSSIBLE_MEMORY_BLOAT",
                         justification = "Only called from <clinit>" )
    private static void initFast( final FastCharset fastCharset )
    {
        INSTANCES.put( fastCharset.name.toLowerCase( Locale.US ), fastCharset );
    }

    // TODO v1.0 Move this.  And fix it.
    /** Nasty utility class to build lookup tables. */
    static class Builder
    {
        /**
         * @param args command-line arguments
         * @throws CharacterCodingException on failure
         */
        public static void main( final String[] args ) throws CharacterCodingException
        {
            for ( String arg : args )
            {
                Charset charset = Charset.forName( arg );
                CharsetEncoder encoder = charset.newEncoder();
                encoder.onMalformedInput( CodingErrorAction.REPLACE ).onUnmappableCharacter( CodingErrorAction.REPLACE );
                encoder.replaceWith( new byte[] { '?' } );


                byte[] table = new byte[ 65536 ];
                char[] reverse = new char[ 256 ];
                Arrays.fill( reverse, '\0' );

                for ( int i = 0; i < 65536; i++ )
                {
                    if ( i >= 0xd800 && i <= 0xdfff )
                    {
                        table[ i ] = '?';
                    }
                    else
                    {
                        ByteBuffer encode = encoder.encode( CharBuffer.wrap( new char[] { (char) i } ) );
                        if ( encode.remaining() != 1 )
                        {
                            throw new AssertionError();
                        }
                        byte b = encode.get( 0 );
                        table[ i ] = b;
                        if ( encoder.canEncode( (char) i ) )
                        {
                            if ( reverse[ b & 0xff ] != 0 )
                            {
                                throw new AssertionError( "DUPLICATE: " + (char) i + " " + reverse[ b & 0xff ] + " " + b);
                            }
                            reverse[ b & 0xff ] = (char) i;
                        }
                    }
                }

                int end;
                for ( end = 65535; end >= 0; end-- )
                {
                    if ( table[ end ] != '?' )
                    {
                        break;
                    }
                }

                System.out.println( "// Charset: " + arg + " [" + end + " entries]" );
                String symName = arg.replace( '-', '_' ).toLowerCase();
                System.out.println( "JNIEXPORT const int dni_charset_forward_table_length_" + symName + ( end + 1 ) );
                System.out.println();
                System.out.println( "static const char dni_charset_forward_table_" + arg.replace( '-', '_' ) + "[ " + ( end + 1 ) + " ] =" );

                int x = 0;
                for ( int i = 0; i <= end; i++ )
                {
                    if ( x == 0 )
                    {
                        System.out.print( "    \"" );
                    }
                    x++;

                    byte b = table[ i ];
                    if ( b >= 32 && b <= 126 && b != '\\' && b != '\"' && b != '\'' )
                    {
                        System.out.print( (char) b );
                    }
                    else
                    {
                        System.out.printf( "\\%03o", b & 0xff );
                        x+=3;
                    }

                    if ( x > 63 || i == end )
                    {
                        x = 0;
                        if ( i == end )
                        {
                            System.out.println( "\";" );
                        }
                        else
                        {
                            System.out.println( '\"' );
                        }
                    }
                }
                System.out.println();
                System.out.println( "JNIEXPORT const char* dni_charset_forward_table_" + arg.replace( '-', '_' ) + " = forward_table_" + arg.replace( '-', '_' ) + ";" );
                System.out.println();


                System.out.println( "statc const jchar reverse_table_" + arg.replace( '-', '_' ) + "[ 256 ] =" );
                System.out.println( '{' );

                x = 0;
                for ( int i = 0; i <= 0xff; i++ )
                {
                    if ( x == 0 )
                    {
                        System.out.print( "   " );
                    }
                    x++;

                    char b = reverse[ i ];

                    System.out.print( ' ' );
                    System.out.printf( "0x%04x", (int) b );
                    System.out.print( ',' );


                    x++;
                    if ( x > 15 || i == 0xff )
                    {
                        x = 0;
                        System.out.println();
                    }
                }
                System.out.println( "};" );
                System.out.println();
                System.out.println( "JNIEXPORT const jchar dni_charset_reverse_table_" + arg.replace( '-', '_' ) + "[ 256 ] = reverse_table_" + arg.replace( '-', '_' ) );
                System.out.println();
            }
        }
    }

}
